
package com.snail.file;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author WuQinglong
 */
public class LogHelper {

    /**
     * 写入缓存
     * 默认值 2M
     */
    private static final byte[] WRITE_BUFFER = new byte[1024 * 1024 * 2];

    /**
     * 缓存写入下标
     */
    private static int bufferIndex = 0;

    /**
     * 多线程操作缓存锁，文件锁
     */
    private static final Lock logLock = new ReentrantLock();
    private static final Lock flushLock = new ReentrantLock();

    /* 日志文件路径 */
    private static final String logDirPath = "/User/wu/logs";
    /* 日志文件命名 */
    private static final String logFileFormat = "demo.log.%s";
    /* 当前正在写入的日志文件命名 */
    private static final String currentLogFileFormat = "demo.log.%s.current";
    /* 当前正在写入的日志文件下标 */
    private static int fileIndex = 0;
    /* 文件的指针 */
    private static long writeOffset = 0;
    /* 当前正在写入的文件 */
    private static String currentFilename = String.format(currentLogFileFormat, fileIndex);
    /* 记录当前正在写入的文件信息 */
    private static final String indexFilename = "file_index";
    private static final String offsetFilename = "file_offset";

    /**
     * 一个日志文件的大小
     * 默认 1G
     * TODO 可配置
     */
    private static int logFileSize = 1024 * 1024 * 1024;

    /**
     * 共可以产生多少个日志文件
     * TODO 可配置
     */
    private static int numberOfLogFiles = 5;

    /**
     * 刷盘线程标志
     */
    public static volatile boolean flushThreadRunning = true;

    /**
     * 刷盘频率
     */
    public static final int flushRate = 1;

    static {
        // 目录是否存在
        File logDir = new File(logDirPath);
        if (!logDir.exists() && !logDir.mkdirs()) {
            // 日志目录不存在，并且新建目录不成功
            throw new RuntimeException("日志目录不存在，并且新建目录不成功");
        }

        // 恢复下标
        recoverWriteStatus();

        // 启动刷盘线程
        Thread flushThread = new Thread(() -> {
            while (flushThreadRunning) {
                flush();
                try {
                    TimeUnit.SECONDS.sleep(flushRate);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "flushDiskThread");
        flushThread.start();
    }

    /**
     * 写入日志信息
     * @param msg 日志
     */
    public static void log(String msg) {
        // 消息使用 UTF8 编码
        byte[] msgBytes = msg.getBytes(StandardCharsets.UTF_8);
        // 写入换行符
        byte[] lineSeparatorBytes = System.lineSeparator().getBytes(StandardCharsets.UTF_8);

        // 写内存锁
        logLock.lock();
        // 这里只有有一个线程可以进入
        try {
            // 校验缓存是否可写，如果不可写，则进行强制刷盘
            if (bufferIndex + msgBytes.length + lineSeparatorBytes.length >= WRITE_BUFFER.length) {
                // 强制刷盘
                flush();
            }

            // 写入到缓存中
            System.arraycopy(msgBytes, 0, WRITE_BUFFER, bufferIndex, msgBytes.length);
            System.arraycopy(lineSeparatorBytes, 0, WRITE_BUFFER, bufferIndex, lineSeparatorBytes.length);
            bufferIndex += msgBytes.length;
            bufferIndex += lineSeparatorBytes.length;
        } finally {
            logLock.unlock();
        }
    }

    /**
     * 刷盘逻辑
     */
    public static void flush() {
        flushLock.lock();

        /*
            有两个线程可以触发刷盘，一个是定时刷盘线程，一个是写日志线程
            如果线程1正在刷盘，线程2卡在等待获取锁，则线程1结束时，线程2进入后，不再需要刷盘操作，只需要判断一些 bufferIndex 是否归 0 即可
         */
        if (bufferIndex == 0) {
            flushLock.unlock();
            return;
        }

        // 写入数据
        RandomAccessFile raf = null;
        try {
            // 待写入的文件
            File logFile = new File(logDirPath, currentFilename);

            // 检查文件是否可写
            long logFileLength = logFile.length();
            if (logFileLength > logFileSize) {
                // 执行新文件

                // 重命名源文件，将 current 去掉
                File destFile = new File(logDirPath, String.format(logFileFormat, fileIndex));
                if (!logFile.renameTo(destFile)) {
                    throw new RuntimeException("日志文件重命名失败");
                }

                // 新的文件下标，形成循环
                fileIndex = fileIndex >= numberOfLogFiles ? 0 : fileIndex + 1;
                flushFileIndex();
                // offset 从 0 开始
                writeOffset = 0;
                // 持久化当前正在写的文件数据
                flushFileOffset();
                // 新的文件名
                currentFilename = String.format(currentLogFileFormat, fileIndex);
                System.out.println("切换当前写入文件：" + currentFilename);

                // 构造新文件的 File 实例
                logFile = new File(logDirPath, currentFilename);
            }

            // 写入缓存数据
            raf = new RandomAccessFile(logFile, "rws");
            raf.seek(writeOffset);
            raf.write(WRITE_BUFFER, 0, bufferIndex);

            // 下次写入位置
            writeOffset += bufferIndex;
            flushFileOffset();

            // 写入后，将缓存写入下标清空
            bufferIndex = 0;
        } catch (Exception e) {
            throw new RuntimeException("缓存写入文件异常", e);
        } finally {
            if (raf != null) {
                try {
                    raf.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            flushLock.unlock();
        }
    }

    private static void flushFileOffset() {
        File file = new File(logDirPath, offsetFilename);
        try (DataOutputStream dos = new DataOutputStream(new FileOutputStream(file, true))) {
            dos.writeLong(writeOffset);
            dos.flush();
        } catch (IOException e) {
            throw new RuntimeException("写入当前写入文件数据异常", e);
        }
    }

    private static void flushFileIndex() {
        File file = new File(logDirPath, indexFilename);
        try (DataOutputStream dos = new DataOutputStream(new FileOutputStream(file))) {
            dos.writeInt(fileIndex);
            dos.flush();
        } catch (IOException e) {
            throw new RuntimeException("写入当前写入文件数据异常", e);
        }
    }

    /**
     * 恢复当前正在写入的文件 以及 文件写入指针
     */
    public static void recoverWriteStatus() {
        File indexFile = new File(logDirPath, indexFilename);
        File offsetFile = new File(logDirPath, offsetFilename);
        if (!indexFile.exists() || !offsetFile.exists()) {
            return;
        }
        try (DataInputStream dis = new DataInputStream(new FileInputStream(indexFile))) {
            fileIndex = dis.readInt();
            currentFilename = String.format(currentLogFileFormat, fileIndex);

        } catch (IOException e) {
            throw new RuntimeException("恢复当前写入文件数据异常", e);
        }
        try (DataInputStream dis = new DataInputStream(new FileInputStream(offsetFile))) {
            writeOffset = dis.readLong();
        } catch (IOException e) {
            throw new RuntimeException("恢复当前写入文件数据异常", e);
        }
        System.out.println("恢复：从文件：" + currentFilename + "的位置：" + writeOffset + "继续写入！");
    }

}
